/**
 *    Copyright 2009-2019 the original author or authors.
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */
package org.apache.ibatis.executor.keygen;

import org.apache.ibatis.binding.MapperMethod.ParamMap;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.executor.ExecutorException;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.reflection.ArrayUtil;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.defaults.DefaultSqlSession.StrictMap;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.TypeHandler;
import org.apache.ibatis.type.TypeHandlerRegistry;

import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Arrays;
import java.util.Collection;
import java.util.Map;

/**
 * @author Clinton Begin
 * @author Kazuki Shimizu
 */
public class Jdbc3KeyGenerator implements KeyGenerator {

    /**
     * A shared instance.
     *
     * @since 3.4.3
     */
    public static final Jdbc3KeyGenerator INSTANCE = new Jdbc3KeyGenerator();

    @Override
    public void processBefore(Executor executor, MappedStatement ms, Statement stmt, Object parameter) {
        // do nothing
    }

    @Override
    public void processAfter(Executor executor, MappedStatement ms, Statement stmt, Object parameter) {
        processBatch(ms, stmt, parameter);
    }

    public void processBatch(MappedStatement ms, Statement stmt, Object parameter) {
        // 获取当前语句执行后需要映射出值的指定属性集合
        final String[] keyProperties = ms.getKeyProperties();
        if (keyProperties == null || keyProperties.length == 0) {
            return;
        }
        try (ResultSet rs = stmt.getGeneratedKeys()) {
            final Configuration configuration = ms.getConfiguration();
            if (rs.getMetaData().getColumnCount() >= keyProperties.length) {
                // 返回结果的列数需要比需要映射属性值的数量要多

                // 获取唯一参数
                Object soleParam = getSoleParameter(parameter);
                if (soleParam != null) {
                    // 入参对象是一个简单对象而不是聚合对象
                    assignKeysToParam(configuration, rs, keyProperties, soleParam);
                } else {

                    assignKeysToOneOfParams(configuration, rs, keyProperties, (Map<?, ?>) parameter);
                }
            }
        } catch (Exception e) {
            throw new ExecutorException("Error getting generated key or setting result to parameter object. Cause: " + e, e);
        }
    }

    protected void assignKeysToOneOfParams(final Configuration configuration, ResultSet rs, final String[] keyProperties,
                                           Map<?, ?> paramMap) throws SQLException {
        // Assuming 'keyProperty' includes the parameter name. e.g. 'param.id'.
        int firstDot = keyProperties[0].indexOf('.');
        if (firstDot == -1) {
            // 只有一个需要设值的参数
            throw new ExecutorException(
                    "Could not determine which parameter to assign generated keys to. "
                            + "Note that when there are multiple parameters, 'keyProperty' must include the parameter name (e.g. 'param.id'). "
                            + "Specified key properties are " + ArrayUtil.toString(keyProperties) + " and available parameters are "
                            + paramMap.keySet());
        }
        // 获取第一个属性的名称
        String paramName = keyProperties[0].substring(0, firstDot);
        // 获取入参对象
        Object param;
        if (paramMap.containsKey(paramName)) {
            // 获取该属性对应的值
            param = paramMap.get(paramName);
        } else {
            throw new ExecutorException("Could not find parameter '" + paramName + "'. "
                    + "Note that when there are multiple parameters, 'keyProperty' must include the parameter name (e.g. 'param.id'). "
                    + "Specified key properties are " + ArrayUtil.toString(keyProperties) + " and available parameters are "
                    + paramMap.keySet());
        }
        // Remove param name from 'keyProperty' string. e.g. 'param.id' -> 'id'
        String[] modifiedKeyProperties = new String[keyProperties.length];
        for (int i = 0; i < keyProperties.length; i++) {
            // 限制多个属性必须属于同一对象
            if (keyProperties[i].charAt(firstDot) == '.' && keyProperties[i].startsWith(paramName)) {
                modifiedKeyProperties[i] = keyProperties[i].substring(firstDot + 1);
            } else {
                throw new ExecutorException("Assigning generated keys to multiple parameters is not supported. "
                        + "Note that when there are multiple parameters, 'keyProperty' must include the parameter name (e.g. 'param.id'). "
                        + "Specified key properties are " + ArrayUtil.toString(keyProperties) + " and available parameters are "
                        + paramMap.keySet());
            }
        }
        // 上面的整个操作目的就是获取用户调用方法时传入的多个入参中需要映射属性集合所属的参数对象。
        assignKeysToParam(configuration, rs, modifiedKeyProperties, param);
    }

    /**
     * 将数据库生产的主键映射到指定对象的属性集合中
     *
     * @param configuration Mybatis的全局Configuration对象
     * @param rs            数据库生成的主键集合所属的ResultSet对象
     * @param keyProperties 所有需要映射值的属性列表
     * @param param         属性列表所属的对象实例
     */
    private void assignKeysToParam(final Configuration configuration, ResultSet rs, final String[] keyProperties,
                                   Object param)
            throws SQLException {
        // 获取类型转换处理器注册表
        final TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
        // 获取返回结果集的元数据
        final ResultSetMetaData rsmd = rs.getMetaData();

        // 将入参对象包装成Collection集合
        // Wrap the parameter in Collection to normalize the logic.
        Collection<?> paramAsCollection = null;
        if (param instanceof Object[]) {
            // 对象数组
            paramAsCollection = Arrays.asList((Object[]) param);
        } else if (!(param instanceof Collection)) {
            // 简单对象
            paramAsCollection = Arrays.asList(param);
        } else {
            // collection集合实例
            paramAsCollection = (Collection<?>) param;
        }
        TypeHandler<?>[] typeHandlers = null;
        // 依次处理入参对象执行设值操作
        for (Object obj : paramAsCollection) {
            if (!rs.next()) {
                // 没有更多的返回结果直接返回
                break;
            }
            // 生成对参数对应的元数据描述类
            MetaObject metaParam = configuration.newMetaObject(obj);
            if (typeHandlers == null) {
                // 获取该参数对应的类型转换处理器
                typeHandlers = getTypeHandlers(typeHandlerRegistry, metaParam, keyProperties, rsmd);
            }
            // 执行设值操作
            populateKeys(rs, metaParam, keyProperties, typeHandlers);
        }
    }

    /**
     * 获取请求参数中的唯一对象，如果请求参数对应的数据中包含了多个对象，
     *
     * @param parameter 用户调用SQl方法传入的实际参数对象
     */
    private Object getSoleParameter(Object parameter) {
        if (!(parameter instanceof ParamMap || parameter instanceof StrictMap)) {
            // 请求入参并不是一个集合类型的参数
            return parameter;
        }

        // return ((HashMap) parameter).size() > 1 ? null : ((HashMap) parameter).values().iterator().next();

        Object soleParam = null;
        for (Object paramValue : ((Map<?, ?>) parameter).values()) {
            if (soleParam == null) {
                soleParam = paramValue;
            } else if (soleParam != paramValue) {
                soleParam = null;
                break;
            }
        }
        // 有可能为null
        return soleParam;
    }

    /**
     * 获取所有所需的类型转换处理器
     *
     * @param typeHandlerRegistry 类型转换处理器注册表
     * @param metaParam           被设值属性所属的对象的元数据
     * @param keyProperties       需要设值的属性集合
     * @param rsmd                数据库生成主键返回结果集的元数据
     */
    private TypeHandler<?>[] getTypeHandlers(TypeHandlerRegistry typeHandlerRegistry, MetaObject metaParam, String[] keyProperties, ResultSetMetaData rsmd) throws SQLException {
        TypeHandler<?>[] typeHandlers = new TypeHandler<?>[keyProperties.length];
        for (int i = 0; i < keyProperties.length; i++) {
            if (metaParam.hasSetter(keyProperties[i])) {
                // 当前对象包含指定属性的setter方法
                Class<?> keyPropertyType = metaParam.getSetterType(keyProperties[i]);
                // 配置类型转换处理器
                typeHandlers[i] = typeHandlerRegistry.getTypeHandler(keyPropertyType, JdbcType.forCode(rsmd.getColumnType(i + 1)));
            } else {
                throw new ExecutorException("No setter found for the keyProperty '" + keyProperties[i] + "' in '"
                        + metaParam.getOriginalObject().getClass().getName() + "'.");
            }
        }
        return typeHandlers;
    }

    private void populateKeys(ResultSet rs, MetaObject metaParam, String[] keyProperties, TypeHandler<?>[] typeHandlers) throws SQLException {
        for (int i = 0; i < keyProperties.length; i++) {
            // 读取属性名称
            String property = keyProperties[i];
            TypeHandler<?> th = typeHandlers[i];
            if (th != null) {
                // 获取数据库生成的主键值
                Object value = th.getResult(rs, i + 1);
                //  设值
                metaParam.setValue(property, value);
            }
        }
    }

}
